iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 23
0
自我挑戰組

從零開始的Flutter世界系列 第 23

Day23 Flutter 的狀態管理 Provider (二)

  • 分享至 

  • xImage
  •  

Provider

參考文件1

參考文件2

前一篇我們提到InheritedWidgetInheritedWidget能夠與它的子孫控件建立依賴關係,並且當InheritedWidget資料發生變化時,可以自動更新有依賴它的子孫控件,而Provider利用這一點來解決跨widget 狀態共享的問題,將需要跨widget 共享的狀態存在InheritedWidget中,然後在子widget 引用InheritedWidget即可

然而InheritedWidget還是存在著一些缺點,像是容易造成不必要的刷新,不支持跨頁面的資料傳遞 (如果不在同一個widget 樹,即無法共享資料),再來就是它共享的資料是不可改變的,如果想讓它監聽資料的變化並且重新構建InheritedWidget,必須透過結合StatefulWidgetChangeNotifierStream來使用

模擬 Provider

我們在Android Studio 建立一個 provider_tutorial 的專案,來模擬Provider大致上運作原理的範例:

建立一個InheritedWidget能保存要共享的跨widget 狀態,其狀態使用泛型,在呼叫的時候會指定具體型別,來讓我們的InheritedWidget通用於各種類型

inherited_provider.dart

import 'package:flutter/material.dart';

class InheritedProvider<T> extends InheritedWidget {
  InheritedProvider({@required this.data, Widget child}) : super(child: child);

  final T data;

  @override
  bool updateShouldNotify(InheritedProvider<T> old) {
    //先都返回true,即每次更新此InheritedWidget,都通知有依賴共享數據的子widget 調用 didChangeDependencies 方法
    return true;
  }
}

接下來我們就要想辦法讓InheritedProvider的資料發生變化的時候,來重新構建InheritedProvider

首先我們透過Flutter SDK中提供的ChangeNotifier類別,它用於向監聽器發送通知。換言之,如果被定義為ChangeNotifier,你可以訂閱它的狀態變化 (即觀察者模式)

ChangeNotifier主要功能:

class ChangeNotifier implements Listenable {
  List listeners=[];
  
  //添加監聽器
  @override
  void addListener(VoidCallback listener) {
     listeners.add(listener);
  }
  
  //移除監聽器
  @override
  void removeListener(VoidCallback listener) {
    listeners.remove(listener);
  }

  //通知所有監聽器,觸發監聽器回調
  void notifyListeners() {
    listeners.forEach((item)=>item());
  }

  ...
}

現在我們將要共享的狀態設計為一個 Model 類,然後讓它繼承ChangeNotifier,這樣當共享的狀態改變時,我們只需要調用notifyListeners()來通知訂閱者,然後由訂閱者來重新構建InheritedProvider

change_notifier_provider.dart

import 'package:flutter/material.dart';

import 'inherited_provider.dart';

// 將要共享的狀態設計為一個 Model 類,然後讓它繼承ChangeNotifier,透過泛型指定共享資料data 型別
class ChangeNotifierProvider<T extends ChangeNotifier> extends StatefulWidget {
  ChangeNotifierProvider({
    Key key,
    this.data,
    this.child,
  });

  final Widget child;
  final T data;

  //提供了靜態方法 of,來給子widget 獲取 InheritedProvider 所保存的共享資料 (設計的Model)
  static T of<T>(BuildContext context) {
    //透過dependOnInheritedWidgetOfExactType 取得InheritedProvider,並指定設計的Model 當作InheritedProvider 的共享狀態型別
    final provider =
        context.dependOnInheritedWidgetOfExactType<InheritedProvider<T>>();
    return provider.data;
  }

  @override
  _ChangeNotifierProviderState<T> createState() =>
      _ChangeNotifierProviderState<T>();
}

//主要作用就是監聽到共享狀態 (model) 改變時重新構建Widget樹
class _ChangeNotifierProviderState<T extends ChangeNotifier>
    extends State<ChangeNotifierProvider<T>> {
  void update() {
    //如果共享資料發生變化 (設計的model 類調用了notifyListeners),重新構建InheritedProvider
    setState(() => {});
  }

  @override
  void didUpdateWidget(ChangeNotifierProvider<T> oldWidget) {
    //當Provider更新資料,將舊資料解除監聽,同時監聽新的資料
    if (widget.data != oldWidget.data) {
      oldWidget.data.removeListener(update);
      widget.data.addListener(update);
    }
    super.didUpdateWidget(oldWidget);
  }

  @override
  void initState() {
    // 給model 新增監聽器
    widget.data.addListener(update);
    super.initState();
  }

  @override
  void dispose() {
    // 移除model 的監聽器
    widget.data.removeListener(update);
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return InheritedProvider<T>(
      data: widget.data,
      child: widget.child,
    );
  }
}

現在CartModel已經通過ChangeNotifierProvider讓我們能夠在widget 上建立監聽、訂閱,但我們該如何去使用它呢,Provider提供Consumer讓我們去完成這一步

Consumer 使用了Builder 模式,當收到更新通知就會通過 builder 重新構建,最好能把Consumer放在widget樹盡量低的位置上,避免UI上任何一點小變化就全盤重新構建widget

consumer.dart

import 'package:flutter/material.dart';

import 'change_notifier_provider.dart';

//Consumer widget唯一必須的參數就是builder
//當ChangeNotifier 發生變化的時候會調用builder這個函數 (就是當我們設計的model 中調用notifyListeners() 時,所有和Consumer相關的builder方法都會被調用)
class Consumer<T> extends StatelessWidget {
  Consumer({
    Key key,
    @required this.builder,
    this.child,
  })  : assert(builder != null),
        super(key: key);

  final Widget child;

  final Widget Function(BuildContext context, T value) builder;

  @override
  Widget build(BuildContext context) {
    return builder(
      context,
      ChangeNotifierProvider.of<T>(context), //取得共享資料 (設計的Model)
    );
  }
}

我們用購物車當範例來使用我們模擬的 Provider:

我們需要實現一個顯示購物車中所有商品總價的功能:向購物車中添加新商品時總價更新

定義一個Item類,用於表示商品信息

cart_item.dart

class Item {
  Item(this.price, this.count);
  double price; 
  int count;
}

定義一個保存購物車內商品資料的CartModel

cart_model.dart

import 'dart:collection';

import 'package:flutter/material.dart';

import 'cart_item.dart';

class CartModel extends ChangeNotifier {
  // 購物車的商品列表
  final List<Item> _items = [];

  // 禁止改變購物車裡的商品資訊
  UnmodifiableListView<Item> get items => UnmodifiableListView(_items);

  double get totalPrice =>
      _items.fold(0, (value, item) => value + item.count * item.price);

  void add(Item item) {
    _items.add(item);
    // 當購物車新增商品時,通知監聽器 (訂閱者),重新構建InheritedProvider,更新狀態
    notifyListeners();
  }
}

main.dart

import 'package:flutter/material.dart';
import 'package:provider_tutorial/cart_model.dart';

import 'cart_item.dart';
import 'change_notifier_provider.dart';
import 'consumer.dart';

void main() {
  runApp(ChangeNotifierProvider<CartModel>(
    data: CartModel(),
    child: MyApp(),
  ));
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Flutter Provider Demo'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Column(
              children: <Widget>[
                Consumer<CartModel>(
                  builder: (context, cart) => Text(
                    "購物車總價: ${cart.totalPrice}",
                    style: Theme.of(context).textTheme.headline5,
                  ),
                ),
                Consumer<CartModel>(
                  builder: (context, cart) => RaisedButton(
                    child: Text("添加商品"),
                    onPressed: () {
                      //添加商品至購物車,添加後總價會更新,先固定用商品為 Item(10.0, 1)當範例
                      ChangeNotifierProvider.of<CartModel>(context)
                          .add(Item(10.0, 1));
                    },
                  ),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

跑出來的結果,每當按下按鈕,即會增加一筆商品 Item(10.0, 1),總價也會改變

此範例只是模擬大概的運作,還是有很多能改善的問題,我們之後就用 Provider Package

Provider package

我們來在pubspec.yaml添加依賴

...
dependencies:
  flutter:
    sdk: flutter

  provider: ^4.3.2
...

我們一樣拿購物車當作範例:

首先一樣建立一個Item類,用於表示商品信息

cart_item.dart

class Item {
  Item(this.price, this.count);
  double price; 
  int count;
}

定義一個保存購物車內商品資料的CartModel

cart_model.dart

import 'dart:collection';

import 'package:flutter/material.dart';

import 'cart_item.dart';

class CartModel extends ChangeNotifier {
  final List<Item> _items = [];

  UnmodifiableListView<Item> get items => UnmodifiableListView(_items);

  double get totalPrice =>
      _items.fold(0, (value, item) => value + item.count * item.price);

  void add(Item item) {
    _items.add(item);
    notifyListeners();
  }
}

接下來我們就可以直接使用 provider,在widget 上建立監聽、訂閱,達成共享狀態資訊等等

main.dart

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:provider_tutorial/cart_model.dart';

import 'cart_item.dart';

void main() {
  runApp(ChangeNotifierProvider(
    create: (context) => CartModel(),
    child: MyApp(),
  ));
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Flutter Demo Home Page'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Consumer<CartModel>(
              builder: (context, cartModel, child) => Text(
                '總價:${cartModel.totalPrice}',
                style: Theme.of(context).textTheme.headline5,
              ),
            ),
            Builder(builder: (context) {
              return RaisedButton(
                child: Text("添加商品"),
                onPressed: () {
                  var counter = context.read<CartModel>();
                  counter.add(Item(10.0, 1));
                },
              );
            })
          ],
        ),
      ),
    );
  }
}

這樣就完成了,跑出來的結果就跟我們模擬的一樣

透過本篇的內容,希望大家能了解對Provider的基本的原理與應用,下一篇將繼續對Provider做補充


上一篇
Day22 Flutter 的狀態管理 Provider (一)
下一篇
Day24 Flutter 的狀態管理 Provider (三)
系列文
從零開始的Flutter世界30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言